package sm.hibernate;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.hibernate.tuple.entity.SMComponentTuplizer;
import org.hibernate.tuple.entity.SMEntityTuplizer;

import sm.dam.AbstractDDLAdaptor;
import sm.dam.api.DDLAdaptor;
import sm.dam.api.DDLAdaptorFactory;
import sm.dbdict.DBDictUtil;
import sm.dbdict.def.DBDict;
import sm.dbdict.def.Field;
import sm.dbdict.def.Key;
import sm.dbdict.def.SqlFieldOptions;
import sm.dbdict.def.SqlTables;
import sm.def.Type;
import sm.hibernate.persister.SMCollectionPersister;
import sm.hibernate.persister.SMEntityPersister;
import sm.hibernate.type.ArrayType;
import sm.hibernate.type.BlobArrayType;
import sm.hibernate.type.BlobCharacterType;
import sm.hibernate.type.CLobCharacterType;
import sm.hibernate.type.CharacterType;
import sm.hibernate.type.LogicalType;
import sm.hibernate.type.NumberType;
import sm.hibernate.type.RCArrayType;
import sm.hibernate.type.RCCharacterType;
import sm.hibernate.type.RCLabelType;
import sm.hibernate.type.RCLogicalType;
import sm.hibernate.type.RCNumberType;
import sm.hibernate.type.RCStructureType;
import sm.hibernate.type.RCTimeType;
import sm.hibernate.type.RCType;
import sm.hibernate.type.TimeType;
import sm.util.Util;

import com.google.gson.Gson;

public class HibernateMappingUtil {

	private static Map<String, Object> getStructureTypeDef(Field field,
			List<Field> subFields) {
		List<Field> childs = DBDictUtil.getChilds(subFields, field, true);
		Map<String, Object> typedef = new LinkedHashMap<String, Object>();
		for (Field child : childs) {
			String name = Util.radFieldNameToJava(child.getName());
			int type = child.getType();
			Object typeObj = type;
			if (type == Type.STRUCTURE_TYPE) {
				List<Field> childSubFields = DBDictUtil.getSubFields(subFields,
						child);
				typeObj = getStructureTypeDef(child, childSubFields);
			} else if (type == Type.ARRAY_TYPE) {
				List<Field> childSubFields = DBDictUtil.getSubFields(subFields,
						child);
				typeObj = getArrayTypeDef(child, childSubFields);
			} else if (type == Type.EXPRESSION_TYPE) {
				// type defined to be expression, is RC encoded as operator
				typeObj = Type.OPERATOR_TYPE;
			}

			typedef.put(name, typeObj);
		}
		return typedef;
	}

	private static List<Object> getArrayTypeDef(Field field,
			List<Field> subFields) {
		List<Object> typedef = new ArrayList<Object>();
		Field typeField = subFields.get(0);
		int type = typeField.getType();
		Object typeObj = type;
		if (type == Type.STRUCTURE_TYPE) {
			List<Field> childSubFields = DBDictUtil.getSubFields(subFields,
					typeField);
			typeObj = getStructureTypeDef(typeField, childSubFields);
		} else if (type == Type.ARRAY_TYPE) {
			List<Field> childSubFields = DBDictUtil.getSubFields(subFields,
					typeField);
			typeObj = getArrayTypeDef(typeField, childSubFields);
		} else if (type == Type.EXPRESSION_TYPE) {
			// type defined to be expression, is RC encoded as operator
			typeObj = Type.OPERATOR_TYPE;
		}

		typedef.add(typeObj);

		return typedef;
	}

	private static Set<String> getAllAliases(List<Field> fields) {
		Set<String> aliases = new HashSet<String>();
		for (Field field : fields) {
			if (DBDictUtil.isDBField(field)) {
				String alias = field.getSql_field_options()
						.getSql_table_alias();
				if (!aliases.contains(alias)) {
					aliases.add(alias);
				}
			}
		}
		return aliases;
	}

	private static Element createPropertyType(Field field,
			List<Field> subFields, DDLAdaptor ddlAdaptor) {
		Element type = DocumentHelper.createElement("type");
		SqlFieldOptions sfo = field.getSql_field_options();
		Boolean rcBoolean = sfo.getSql_rc_format();
		boolean rc = rcBoolean != null ? rcBoolean.booleanValue() : false;
		int fieldType = field.getType();
		switch (fieldType) {
		case Type.NUMBER_TYPE:
			if (rc) {
				type.addAttribute("name", RCNumberType.class.getName());
			} else {
				type.addAttribute("name", NumberType.class.getName());
			}
			break;
		case Type.CHARACTER_TYPE:
			if (rc) {
				type.addAttribute("name", RCCharacterType.class.getName());
			} else if (ddlAdaptor.isCLob(sfo.getSql_data_type())) {
				type.addAttribute("name", CLobCharacterType.class.getName());
			} else if (ddlAdaptor.isBLob(sfo.getSql_data_type())) {
				type.addAttribute("name", BlobCharacterType.class.getName());
			} else {
				type.addAttribute("name", CharacterType.class.getName());
			}
			break;
		case Type.TIME_TYPE:
			if (rc) {
				type.addAttribute("name", RCTimeType.class.getName());
			} else {
				type.addAttribute("name", TimeType.class.getName());
			}
			break;
		case Type.LOGICAL_TYPE:
			if (rc) {
				type.addAttribute("name", RCLogicalType.class.getName());
			} else {
				type.addAttribute("name", LogicalType.class.getName());
			}
			break;
		case Type.LABEL_TYPE:
			if (rc) {
				type.addAttribute("name", RCLabelType.class.getName());
			} else if (ddlAdaptor.isCLob(sfo.getSql_data_type())) {
				type.addAttribute("name", CLobCharacterType.class.getName());
			} else if (ddlAdaptor.isBLob(sfo.getSql_data_type())) {
				type.addAttribute("name", BlobCharacterType.class.getName());
			} else {
				type.addAttribute("name", CharacterType.class.getName());
			}

			break;
		case Type.STRUCTURE_TYPE:
			if (rc) {
				type.addAttribute("name", RCStructureType.class.getName());
				boolean isDescriptor = false;
				int level = field.getLevel();
				if (level == 0) {
					isDescriptor = true;
				}
				Element isDescriptorParam = type.addElement("param");
				isDescriptorParam.addAttribute("name", "descriptor");
				isDescriptorParam.addText(Boolean.toString(isDescriptor));
				Element typedefParam = type.addElement("param");
				typedefParam.addAttribute("name", "typedef");
				Map<String, Object> typedef = getStructureTypeDef(field,
						subFields);
				Gson gson = new Gson();
				typedefParam.addText(gson.toJson(typedef, Map.class));
			}
			break;
		case Type.ARRAY_TYPE:
			sfo = subFields.get(0).getSql_field_options();
			if (rc) {
				type.addAttribute("name", RCArrayType.class.getName());
			} else if (ddlAdaptor.isBLob(sfo.getSql_data_type())) {
				type.addAttribute("name", BlobArrayType.class.getName());
			} else {
				type.addAttribute("name", ArrayType.class.getName());
			}
			Element typedefParam = type.addElement("param");
			typedefParam.addAttribute("name", "fieldTypeDef");
			Object fieldTypedef = getArrayTypeDef(field, subFields).get(0);
			Gson gson = new Gson();
			typedefParam.addText(gson.toJson(fieldTypedef, Object.class));
			break;
		case Type.EXPRESSION_TYPE:
		case Type.RECORD_TYPE:
			type.addAttribute("name", RCType.class.getName());
			break;
		default:
			throw new RuntimeException("Unsupported type: " + fieldType);
		}
		return type;
	}

	private static Element createTuplizer(String className) {
		Element tuplizer = DocumentHelper.createElement("tuplizer");
		tuplizer.addAttribute("class", className);
		tuplizer.addAttribute("entity-mode", "dynamic-map");
		return tuplizer;
	}

	/**
	 * 
	 * @param field
	 * @param allFields
	 * @param aliasMapping
	 * @param primaryFields
	 * @param parentComponentType
	 *            0:dynamic-component/class, 1:composite-element,
	 *            2:nested-composite-element
	 */
	private static void addFieldMapping(Field field, List<Field> allFields,
			Map<String, Element> aliasMapping, int parentComponentType,
			final Map<String, Element> arrayMapping,
			final String primaryTableAlias, final List<Field> primaryFields,
			DBDict dbdict) {
		// skip primary fields
		if (primaryFields.contains(field)) {
			return;
		}

		// get field infos
		boolean rc = false;
		String fieldAlias = null;
		if (DBDictUtil.isDBField(field)) {
			Boolean rcBool = field.getSql_field_options().getSql_rc_format();
			if (rcBool != null) {
				rc = rcBool;
			}
			fieldAlias = field.getSql_field_options().getSql_table_alias();
		}
		String columnName = DBDictUtil.isDBColumn(field) ? field
				.getSql_field_options().getSql_column_name() : null;
		int type = field.getType();

		// use field path as field name for joined fields
		String fieldName = field.getName();
		if (fieldAlias != null) {
			// join table
			if (parentComponentType == 0
					&& !primaryTableAlias.equals(fieldAlias)
					&& !arrayMapping.containsKey(fieldAlias)) {
				fieldName = DBDictUtil.getFieldPath(field);
			}
		}
		fieldName = Util.radFieldPathToJava(fieldName);

		// get ddl adaptor for the db type
		SqlTables table = null;
		if (fieldAlias != null) {
			table = DBDictUtil.getTableByAlias(dbdict.getSql_tables(),
					fieldAlias);
		} else {
			table = DBDictUtil.getTableByAlias(dbdict.getSql_tables(),
					primaryTableAlias);
		}
		DDLAdaptor ddlAdaptor = DDLAdaptorFactory.getDDLAdaptor(table
				.getSql_db_type());

		if (type == Type.STRUCTURE_TYPE) {
			int level = field.getLevel();
			boolean isDescriptor = false;
			if (level == 0) {
				isDescriptor = true;
			}

			List<Field> subFields = DBDictUtil.getSubFields(allFields, field);
			if (rc) {
				Element mapping = aliasMapping.get(fieldAlias);
				// skip null table
				if (mapping != null) {
					Element property = mapping.addElement("property");
					property.addAttribute("name", fieldName);
					property.addAttribute("column",
							ddlAdaptor.toSqlEntityName(columnName));

					Element propertyType = createPropertyType(field, subFields,
							ddlAdaptor);
					property.add(propertyType);
				}
			}

			Map<String, Element> fieldAliasMapping = null;
			if (isDescriptor) {
				fieldAliasMapping = aliasMapping;
			} else {
				Set<String> aliases = getAllAliases(subFields);
				fieldAliasMapping = new HashMap<String, Element>();

				Element primaryComponent = DocumentHelper
						.createElement("component");
				primaryComponent.addAttribute("name", fieldName);
				primaryComponent.add(createTuplizer(SMComponentTuplizer.class
						.getName()));
				boolean needPrimaryComponent = false;
				for (String alias : aliases) {
					Element mapping = aliasMapping.get(alias);
					// skip null table
					if (mapping != null) {
						Element component = mapping;
						if (alias.equals(primaryTableAlias)) {
							// primary table
							component = primaryComponent;
							needPrimaryComponent = true;
						} else {
							if (parentComponentType == 0) {
								// array table's parent
								if (arrayMapping.containsKey(alias)) {
									component = primaryComponent;
									needPrimaryComponent = true;
								}
								// skip join tables
							} else if (parentComponentType == 1) {
								component = mapping
										.addElement("composite-element");
								component.addAttribute("class",
										Map.class.getName());
								component
										.add(createTuplizer(SMComponentTuplizer.class
												.getName()));
								parentComponentType = 2;
							} else if (parentComponentType == 2) {
								component = mapping
										.addElement("nested-composite-element");
								component.addAttribute("class",
										Map.class.getName());
								component.addAttribute("name", fieldName);
								component
										.add(createTuplizer(SMComponentTuplizer.class
												.getName()));
							}
						}
						fieldAliasMapping.put(alias, component);
					}
				}

				if (needPrimaryComponent) {
					Element mapping = aliasMapping.get(primaryTableAlias);
					// skip null table
					if (mapping != null) {
						mapping.add(primaryComponent);
					}
				}

			}

			List<Field> childs = DBDictUtil.getChilds(subFields, field, false);
			for (Field child : childs) {
				addFieldMapping(child, subFields, fieldAliasMapping,
						parentComponentType, arrayMapping, primaryTableAlias,
						primaryFields, dbdict);
			}
		} else if (type == Type.ARRAY_TYPE) {
			List<Field> subFields = DBDictUtil.getSubFields(allFields, field);
			Field subField = subFields.get(0);
			if (!DBDictUtil.isDBField(field) && DBDictUtil.isDBField(subField)) { // CLOB
				String subFieldAlias = subField.getSql_field_options()
						.getSql_table_alias();
				Element mapping = aliasMapping.get(subFieldAlias);
				// skip null table
				if (mapping != null) {
					Element property = mapping.addElement("property");
					property.addAttribute("name", fieldName);
					columnName = subField.getSql_field_options()
							.getSql_column_name();
					property.addAttribute("column",
							ddlAdaptor.toSqlEntityName(columnName));
					Element propertyType = createPropertyType(field, subFields,
							ddlAdaptor);
					property.add(propertyType);
				}
			} else {
				if (rc) {
					Element mapping = aliasMapping.get(fieldAlias);
					// skip null table
					if (mapping != null) {
						Element property = mapping.addElement("property");
						property.addAttribute("name", fieldName);
						property.addAttribute("column",
								ddlAdaptor.toSqlEntityName(columnName));
						Element propertyType = createPropertyType(field,
								subFields, ddlAdaptor);
						property.add(propertyType);
					}
				}

				if (fieldAlias != null && columnName == null) { // joined array
					// use primary table as parent
					Element mapping = aliasMapping.get(primaryTableAlias);
					// skip null table
					if (mapping != null) {
						Element list = arrayMapping.get(fieldAlias);
						list.addAttribute("name", fieldName);
						mapping.add(list);
						if (subField.getType() == Type.STRUCTURE_TYPE) {
							Map<String, Element> fieldAliasMapping = new HashMap<String, Element>();
							fieldAliasMapping.put(fieldAlias, list);
							addFieldMapping(subField, subFields,
									fieldAliasMapping, 1, arrayMapping,
									primaryTableAlias, primaryFields, dbdict);
						} else {
							Element element = list.addElement("element");
							for (Field sField : subFields) {
								if (DBDictUtil.isDBColumn(sField)) {
									element.addAttribute("column", ddlAdaptor
											.toSqlEntityName(sField
													.getSql_field_options()
													.getSql_column_name()));
									subField = sField;
									break;
								}
							}

							Element propertyType = createPropertyType(subField,
									null, ddlAdaptor);
							element.add(propertyType);
						}
					}
				}

			}
		} else if (columnName != null) {
			Element mapping = aliasMapping.get(fieldAlias);
			// skip null table
			if (mapping != null) {
				Element property = mapping.addElement("property");
				property.addAttribute("name", fieldName);
				property.addAttribute("column",
						ddlAdaptor.toSqlEntityName(columnName));
				Element propertyType = createPropertyType(field, null,
						ddlAdaptor);
				property.add(propertyType);
			}
		}

	}

	public static Document generateHBM(DBDict dbdict) throws IOException {
		Document doc = DocumentHelper.createDocument();
		doc.addDocType("hibernate-mapping",
				"-//Hibernate/Hibernate Mapping DTD//EN",
				"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd");
		Element root = doc.addElement("hibernate-mapping");
		Element mapping = root.addElement("class");
		mapping.add(createTuplizer(SMEntityTuplizer.class.getName()));

		String fileName = dbdict.getName();
		String entityName = Util.radEntityNameToJava(fileName);
		mapping.addAttribute("entity-name", entityName);

		SqlTables primaryTable = DBDictUtil.getPrimaryTable(dbdict);
		DDLAdaptor ddlAdaptor = DDLAdaptorFactory.getDDLAdaptor(primaryTable
				.getSql_db_type());
		String primaryTableName = primaryTable.getSql_table_name();
		String primaryTableAlias = primaryTable.getSql_table_alias();
		mapping.addAttribute("table", primaryTableName);

		mapping.addAttribute("persister", SMEntityPersister.class.getName());

		// generate key mapping
		List<Field> fields = dbdict.getField();
		Key primaryKey = DBDictUtil.getPrimaryKey(dbdict);
		List<Field> primaryFields = null;
		if (primaryKey == null) {
			primaryFields = Collections.EMPTY_LIST;
		} else {
			primaryFields = DBDictUtil.getKeyFields(primaryKey, fields);
		}
		int primaryFieldsCount = primaryFields.size();
		if (primaryFieldsCount == 0) {
			Element id = mapping.addElement("id");
			id.addAttribute("column",
					ddlAdaptor.toSqlEntityName(AbstractDDLAdaptor.RECORD_KEY));
			id.addAttribute("type", "long");
			Element generator = id.addElement("generator");
			generator.addAttribute("class", "increment");
		} else if (primaryFieldsCount == 1) { // id
			Field primaryField = primaryFields.get(0);
			String primaryKeyName = DBDictUtil.getFieldPath(primaryField);
			Element id = mapping.addElement("id");
			id.addAttribute("name", Util.radFieldPathToJava(primaryKeyName));
			id.addAttribute("column", ddlAdaptor.toSqlEntityName(primaryField
					.getSql_field_options().getSql_column_name()));
			Element propertyType = null;
			if (Type.isPrimitiveType(primaryField.getType())) {
				propertyType = createPropertyType(primaryField, null,
						ddlAdaptor);
			} else {
				List<Field> subFields = DBDictUtil.getSubFields(fields,
						primaryField);
				propertyType = createPropertyType(primaryField, subFields,
						ddlAdaptor);
			}
			id.add(propertyType);
		} else {
			Element compositeId = mapping.addElement("composite-id");
			for (int i = 0; i < primaryFields.size(); i++) {
				Element keyProperty = compositeId.addElement("key-property");
				Field primaryField = primaryFields.get(i);
				String primaryKeyName = DBDictUtil.getFieldPath(primaryField);
				keyProperty.addAttribute("name",
						Util.radFieldPathToJava(primaryKeyName));
				keyProperty.addAttribute("column", ddlAdaptor
						.toSqlEntityName(primaryField.getSql_field_options()
								.getSql_column_name()));
				Element propertyType = null;
				if (Type.isPrimitiveType(primaryField.getType())) {
					propertyType = createPropertyType(primaryField, null,
							ddlAdaptor);
				} else {
					List<Field> subFields = DBDictUtil.getSubFields(fields,
							primaryField);
					propertyType = createPropertyType(primaryField, subFields,
							ddlAdaptor);
				}
				keyProperty.add(propertyType);
			}
		}

		Map<String, Element> aliasMapping = new HashMap<String, Element>();
		Map<String, Element> arrayMapping = new HashMap<String, Element>();
		aliasMapping.put(primaryTableAlias, mapping);
		List<SqlTables> tables = dbdict.getSql_tables();
		for (SqlTables table : tables) {
			String tableAlias = table.getSql_table_alias();
			String tableName = table.getSql_table_name();
			if (!primaryTableAlias.equals(tableAlias)) {
				List<Field> tableFields = DBDictUtil.getTableFields(fields,
						tableAlias);
				// skip null table
				if (tableFields.size() > 0
						&& !DDLAdaptor.NULLTABLE.equals(tableName)) {
					if (DBDictUtil.isArrayTable(tableFields)) {
						Element list = DocumentHelper.createElement("list");
						// list.addAttribute("inverse", "false");
						list.addAttribute("table", tableName);
						list.addAttribute("persister",
								SMCollectionPersister.class.getName());
						Element key = list.addElement("key");
						if (primaryFieldsCount == 0) {
							Element column = key.addElement("column");
							column.addAttribute(
									"name",
									ddlAdaptor
											.toSqlEntityName(AbstractDDLAdaptor.RECORD_KEY));
						} else {
							for (Field primaryField : primaryFields) {
								Element column = key.addElement("column");
								column.addAttribute("name", ddlAdaptor
										.toSqlEntityName(primaryField
												.getSql_field_options()
												.getSql_column_name()));
							}
						}

						Element listIndex = list.addElement("list-index");
						listIndex.addAttribute("column", ddlAdaptor
								.toSqlEntityName(DDLAdaptor.RECORD_NUMBER));
						listIndex.addAttribute("base", "1");
						arrayMapping.put(tableAlias, list);
					} else {
						Element join = DocumentHelper.createElement("join");
						join.addAttribute("table", tableName);
						join.addAttribute("fetch", "select");
						Element key = join.addElement("key");
						for (Field primaryField : primaryFields) {
							Element column = key.addElement("column");
							column.addAttribute("name", ddlAdaptor
									.toSqlEntityName(primaryField
											.getSql_field_options()
											.getSql_column_name()));
						}
						aliasMapping.put(tableAlias, join);
					}
				}
			}
		}

		// list belongs to the main mapping
		for (String arrayAlias : arrayMapping.keySet()) {
			aliasMapping.put(arrayAlias, mapping);
		}

		Field descriptor = fields.get(0);
		addFieldMapping(descriptor, fields, aliasMapping, 0, arrayMapping,
				primaryTableAlias, primaryFields, dbdict);

		for (Entry<String, Element> entry : aliasMapping.entrySet()) {
			String alias = entry.getKey();
			if (!alias.equals(primaryTableAlias)) {
				if (!arrayMapping.containsKey(alias)) {
					// is join
					Element join = entry.getValue();
					mapping.add(join);
				}
			}
		}

		return doc;
	}

	public static File saveHBM(String fileName, File dir, Document document)
			throws IOException {
		File newHbmFile = new File(dir, fileName + ".hbm.xml");
		if (newHbmFile.exists()) {
			newHbmFile.delete();
		}
		FileOutputStream os = new FileOutputStream(newHbmFile);
		OutputFormat of = OutputFormat.createPrettyPrint();
		XMLWriter writer = new XMLWriter(os, of);
		writer.write(document);
		return newHbmFile;
	}
}
